import { isSameVnode } from './index'
export function patch(oldVnode, vnode) {
    //console.log(oldVnode);
    //console.log(vnode);
    //console.log(vnode);

    //1.组件 oldVnode是null, $mount()
    if (!oldVnode) {
        //根据虚拟节点创建真实节点
        return createElm(vnode)
    }
    //debugger
    //2.初渲染 oldVnode 真实dom元素
    const isRealElement = oldVnode.nodeType;
    if (isRealElement) {
        //初次渲染
        const oldElm = oldVnode;//真实的dom元素 <div id="app"></div>
        const parentElm = oldElm.parentNode; //body  
        let el = createElm(vnode);//将虚拟的vnode树,转化成真实dom
        parentElm.insertBefore(el, oldElm.nextSibling);//将这个转化后的真实dom放在旧的dom后面
        parentElm.removeChild(oldElm);//删除这个旧dom元素
        return el;//vm.$el
    } else {
        //3.diff算法 两个虚拟dom对比
        //对比1:如果两个虚拟节点的标签名不同,直接替换
        //div=>p
        if (oldVnode.tag !== vnode.tag) {
            //标签不一样,找到 旧的vnode 身上的 真实dom 替换成 新的真实dom
            return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
        }

        //对比2:标签一样但是为两个文本元素 {tag:undefined,text} {tag:undefined,text}
        if (!oldVnode.tag) {//标签相同,且是文本
            if (oldVnode.text !== vnode.text) {
                return oldVnode.el.textContent = vnode.text
            }
        }
        //对比3:标签元素一样,复用老节点,属性不同,更新属性
        let el = vnode.el = oldVnode.el
        //oldVnode的属性和newVnode节点进行对比
        updateProperties(vnode, oldVnode.data)
        //对比4:更新子元素
        let oldChilren = oldVnode.children || [];
        let newChilren = vnode.children || [];
        if (oldChilren.length > 0 && newChilren.length > 0) {
            //4.1 oldVnode 和 newVnode 都有儿子----dom-diff
            updateChilren(el, oldChilren, newChilren);
        } else if (oldChilren.length > 0) {
            //4.2 oldVnode 有儿子, newVnode 没儿子----删除oldVnode儿子
            el.innerHTML = ''//清空删除所有节点
        } else if (newChilren.length > 0) {
            //4.3 oldVnode 没儿子, newVnode 有儿子 ----oldVnode上添加儿子
            newChilren.forEach(child => el.appendChild(createElm(child)))
        }
    }
}
//更新子元素的diff算法
function updateChilren(parent, oldChilren, newChilren) {
    //首尾双指针
    let oldStartIndex = 0;//oldVnode 老的首索引
    let oldEndIndex = oldChilren.length - 1;//oldVnode 老的尾索引
    let oldStartNode = oldChilren[0];//oldVnode 老的首节点
    let oldEndNode = oldChilren[oldEndIndex]//oldVnode 老的尾节点
    //---------------------------------------------------------------------------
    let newStartIndex = 0;//newVnode 新的首索引
    let newEndIndex = newChilren.length - 1;//newVnode 新的尾索引
    let newStartNode = newChilren[0];//newVnode 新的首节点
    let newEndNode = newChilren[newEndIndex]//newVnode 新的尾节点
    //配置一个虚拟dom的index和key的映射表,{A:0,B:1,C:2 ..}
    function makeIndexByKey(oldChilren) {
        let map = {};
        oldChilren.forEach((item, index) => {
            map[item.key] = index;
        })
        return map;
    }
    //获取该配置表
    let map = makeIndexByKey(oldChilren);
    //console.log(map);

    //新旧vdom节点的指针重合,指针移动完毕,结束对比
    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        //如果是空节点,首指针向后移动
        if (!oldStartNode) {
            oldStartNode = oldChilren[++oldStartIndex];
        } else if (!oldEndNode) {
            //尾指针向前移动
            oldEndNode = oldChilren[--oldEndIndex];
        } else if (isSameVnode(oldStartNode, newStartNode)) {
            //diff算法策略:尾部插入/头部插入/头部移动至尾部/尾部移动至头部/乱序
            //1.尾部插入
            //oldVnode和newVnode尾指针不变,首指针前往后 移动指针.用key值和tag标签判断是相同的节点
            //递归对比节点(子元素的diff算法)
            patch(oldStartNode, newStartNode)
            //移动oldVnode的首指针====>oldVnode的首指针+1,更新oldVnode首节点
            oldStartNode = oldChilren[++oldStartIndex]
            //移动newVnode的首指针====>newVnode的首指针+1,更新newVnode首节点
            newStartNode = newChilren[++newStartIndex]
        } else if (isSameVnode(oldEndNode, newEndNode)) {
            //2.头部插入
            //oldVnode和newVnode首指针不变,尾指针后往前 移动指针.用key值和tag标签判断是相同的节点
            patch(oldEndNode, newEndNode);
            oldEndNode = oldChilren[--oldEndIndex];
            newEndNode = newChilren[--newEndIndex];
        } else if (isSameVnode(oldStartNode, newEndNode)) {
            //3.头部移动至尾部
            //oldVnode的首指针和newVnode尾指针对比,oldVnode的首指针向后移动,newVnode尾指针向前移动
            patch(oldStartNode, newEndNode);
            parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling);
            oldStartNode = oldChilren[++oldStartIndex];
            newEndNode = newChilren[--newEndIndex];
        } else if (isSameVnode(oldEndNode, newStartNode)) {
            //4.尾部移动至头部
            //oldVnode的尾指针和newVnode的头指针对比,oldVnode的尾指针往前移动,newVnode的头指针往后移动
            patch(oldEndNode, newStartNode);
            parent.insertBefore(oldEndNode.el, oldStartNode.el);
            oldEndNode = oldChilren[--oldEndIndex];
            newStartNode = newChilren[++newStartIndex];
        } else {
            //5.乱序
            //获取映射表中的index,即新旧两个虚拟dom有相同元素
            let moveInx = map[newStartNode.key]
            //moveInx==undfiend,不存在oldVnode的映射表中,
            if (moveInx == undefined) {
                //将这个newVnode首节点插入至,oldVnode首节点前面
                parent.insertBefore(createElm(newStartNode), oldStartNode.el)
            } else {
                //找到移动的需要移动的节点
                let moveVnode = oldChilren[moveInx];
                //将需要的移动的节点位置置空,反正数组塌陷
                oldChilren[moveInx] = null;
                //更新对比 这个移动的元素和newVnode
                patch(moveVnode, newStartNode);
                //在这个oldVnode的元素前面插入这个需要移动的节点
                parent.insertBefore(moveVnode.el, oldStartNode.el)
            }
            //移动指针
            newStartNode = newChilren[++newStartIndex];
        }
    }
    /**
     尾部插入 ===> 在oldVnode 尾部插入更新,newVnode元素 一定大于 oldVnode元素
     oldVnode:① ② ③ ④ ⑤
     newVnode:① ② ③ ④ ⑤ | ⑥ ⑦
    **/
    //-----------------------------------------------------------------------
    /**
      头部插入 ===> 在oldVnode 头部插入更新,newVnode元素 一定大于 oldVnode元素
      oldVnode:      ① ② ③ ④ ⑤
      newVnode:⑥ ⑦ | ① ② ③ ④ ⑤ 
    **/
    /* 
        尾部插入/头部插入的总结
        1.oldVnode元素少于newVnode元素 
        2.尾指针不变,头指针前往后移动(尾部插入)/头指针不变,尾指针后往前移动(头部插入)
        3.当newVnode首指针索引小于等于newVnode尾指针索引
        4.判断newVnode的尾指针的索引后一位是否为null,
            1)是则返回null,说明是尾部插入
            2)不是则返回 索引后一位的vnode的真实dom,说明是头部插入,该真实dom就是oldVnode的头指针
        5.调用insertBefore方法插入真实dom元素
            1)parent.insertBefore(arg1,arg2),将arg1插至arg2前面
            2)parent.insertBefore(arg1,null),将arg1插至parent末尾,类似于parent.appendChild()
    */
    //-----------------------------------------------------------------------
    /**
        头部移动至尾部 ===> oldVnode的首指针和newVnode尾指针对比,oldVnode的首指针向后移动,newVnode尾指针向前移动
        oldVnode:① ② ③ ④ ⑤
        newVnode:② ③ ④ ⑤ ① 
        调用insertBefore方法,在oldVnode的尾节点的下一个元素前面添加这个首节点元素
    **/
    //-----------------------------------------------------------------------
    /**
        尾部移动至头部 ===> oldVnode的首指针和newVnode尾指针对比,oldVnode的首指针向后移动,newVnode尾指针向前移动
        oldVnode:① ② ③ ④ ⑤
        newVnode:⑤ ① ② ③ ④ 
        调用insertBefore方法,在oldVnode的头节点前面添加这个尾节点元素
    **/
    //-----------------------------------------------------------------------
    /*  
        乱序 ===> 头头对比,尾尾对比,首尾对比,尾首对比==>最后根据newVnode的key查找oldVnode的映射表
                ===> 找到则存储这个变量,将这个oldVnode转化成真实dom树,移动至当前的OldVnode首节点的前面,这个oldVnode的位置要置空,防止数组塌陷
                ===> 找不到将newVnode的真实元素插入至oldVnode的首节点前

        oldVnode：① ② ③ ④ ⑤
        newVnode: ⑦ ① ③ ② ④ 
    */
    //newVnode元素大于oldVnode元素,插入/移动
    if (newStartIndex <= newEndIndex) {
        for (let i = newStartIndex; i <= newEndIndex; i++) {
            let nextEle = newChilren[newEndIndex + 1] == null ? null : newChilren[newEndIndex + 1].el
            //parent.appendChild(cnreateElm(ewChilren[i]))
            parent.insertBefore(createElm(newChilren[i]), nextEle);
        }
    }
    //newVnode元素小于oldVnode元素,删除
    if (oldStartIndex <= oldEndIndex) {
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
            let child = oldChilren[i];
            if (child != undefined) {
                parent.removeChild(child.el)
            }
        }
    }
}
//更新属性样式
function updateProperties(vnode, oldProps = {}) {
    //获取虚拟dom树
    let newProps = vnode.data || {};
    //获取挂载在虚拟dom上面的真实dom树
    let el = vnode.el;
    //属性对比1:老的属性 新的没有 删除属性
    for (let key in oldProps) {
        if (!newProps[key]) {
            el.removeAttribute(key)
        }
    }
    //获取新老属性
    let newStyle = newProps.style || {};
    let oldStyle = oldProps.style || {};
    //样式属性,特殊处理,新老属性对比
    for (let key in oldStyle) {
        if (!newStyle[key]) {
            el.style[key] = ''
        }
    }
    //debugger;
    //属性对比2:新的属性,老的没有,直接用新属性覆盖
    //循环当前这个虚拟dom树的data属性
    for (let key in newProps) {
        //当属性为style时
        if (key == 'style') {
            //newProps{style:{ color:red ,background:green}}
            for (let styleName in newProps.style) {
                //style:{ color:red ,background:green}
                //操作真实dom元素style,动态添加
                el.style[styleName] = newProps.style[styleName];
            }
        } else if (key == 'class') {
            //class:{ color:red ,background:green}
            //操作真实dom元素className,动态添加
            el.className = newProps.class
        } else {
            //操作真实dom元素,动态添加属性
            el.setAttribute(key, newProps[key])
        }
    }
    //debugger;
}


//判断是否为组件,是的话将虚拟的组件vnode => 真实的dom,不是返回false
function createComponent(vnode) {
    //判断是否为组件,组件身上有hooks属性
    let i = vnode.data;
    //先取hooks再取init
    if ((i = i.hook) && (i = i.init)) {
        /*
            (i = i.hook) && (i = i.init)
            let i=vnode.data;
            i=vnode.data.hooks;
            i=i.init(===vnode.data.hook.init)
        */
        i(vnode);//调用组件的初始化方法
    }
    if (vnode.componentInstance) {
        //如果虚拟节点上有组件的实例说明当前这个vnode是组件
        return true;
    }
    return false;
}


//将虚拟的vnode树,转化成真实dom
export function createElm(vnode) {
    let { tag, children, key, data, text, vm } = vnode;
    //debugger;
    //如果是tag标签
    if (typeof tag === 'string') {//
        //也可能是组件,如果是组件,就直接根据组件创建出组件对应的真实节点
        //将组件虚拟节点,转化成真实节点
        if (createComponent(vnode)) {//如果为true,说明这个虚拟节点是组件
            //如果是组件,就将渲染后组件真实组件返回
            console.log(vnode.componentInstance);
            return vnode.componentInstance.$el
        }
        //根据虚拟dom的tag标签.创建成真实dom的tag标签,并且挂载在这个虚拟dom的el属性上
        vnode.el = document.createElement(tag);
        //更新属性/样式
        updateProperties(vnode);
        //debugger
        //如果有子标签
        //console.log(children);
        children.forEach(child => {
            //这个真实dom上递归添加子标签
            vnode.el.appendChild(createElm(child));
        })
    } else {
        //如果是文本
        //根据虚拟dom的test文本.创建成真实dom的test文本,并且挂载在这个虚拟dom的el属性上
        vnode.el = document.createTextNode(text);
    }

    return vnode.el;
}
